/**************************************************************************\
    Copyright Microsoft Corporation. All Rights Reserved.
\**************************************************************************/

namespace Microsoft.ContactsBridge
{
    using Microsoft.ContactsBridge.Interop;
    using Standard;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Text;

    // Note that this won't work with simple extensions
    // because of the different syntax for creating array nodes.
    // It could be generalized by making the constructor smarter to parse external namespaces,
    // but I don't intend to expose this class outside the assembly and am only providing
    // schema defined property collections.
    internal class SchematizedLabeledPropertyCollection<T> : ILabeledPropertyCollection<T>
    {
        public delegate T TypeCreator(Contact contact, string arrayNode);
        public delegate void TypeCommitter(Contact contact, string arrayNode, T value);

        private Contact _contact;
        private TypeCreator _creatorDelegate;
        private TypeCommitter _committerDelegate;
        private string _collection;
        private string _node;
        private string _formatNode;
        private int _count;

        private void _OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            _count = -1;
        }

        private string _GetIndexedPropety(int index)
        {
            if (index >= Count)
            {
                throw new ArgumentOutOfRangeException("index", index, "The index is invalid");
            }
            return string.Format(_formatNode, index+1);
        }

        public SchematizedLabeledPropertyCollection(Contact contact, string collectionType, string nodeName, TypeCreator creator, TypeCommitter committer)
        {
            Validate.NotNull(contact, "contact");
            Validate.NotNull(collectionType, "collectionType");
            Validate.NotNull(nodeName, "nodeName");
            Validate.NotNull(creator, "creator");

            _contact = contact;
            _contact.PropertyChanged += _OnPropertyChanged;
            _collection = collectionType;
            _creatorDelegate = creator;
            _committerDelegate = committer;
            _node = nodeName;
            _formatNode = _collection + nodeName + "[{0}]";
            Assert.IsTrue(ContactUtil.IsPropertyValidNode(string.Format(_formatNode, 1)));

            _count = -1;
        }

        #region ILabeledPropertyCollection<T> Members

        public T Default
        {
            get
            {
                return this[(string[])null];
            }
            set
            {
                int index = DefaultIndex;
                if (-1 == index)
                {
                    this[(string[])null] = value;
                    index = DefaultIndex;
                    Assert.AreNotEqual(-1, index);
                }
                else
                {
                    this[index] = value;
                }
                DefaultIndex = index;
            }
        }

        public int DefaultIndex
        {
            get
            {
                string node;
                HRESULT hr = ContactUtil.GetLabeledNode((IContactProperties)_contact.IContact, _collection, null, out node);
                if ((HRESULT)Win32Error.ERROR_PATH_NOT_FOUND == hr)
                {
                    return -1;
                }
                // If this failed for a reason other than that the node isn't present, throw the exception.
                hr.ThrowIfFailed("Unknown failure while searching for the preferred array arrayNode.");

                // returns zero-based index.
                return ContactUtil.GetIndexFromNode(node);
            }
            set
            {
                for (int i = 0; i < Count; ++i)
                {
                    if (value == i)
                    {
                        GetLabelsAt(i).Add(PropertyLabels.Preferred);
                    }
                    else
                    {
                        GetLabelsAt(i).Remove(PropertyLabels.Preferred);
                    }
                }
            }
        }

        public ILabelCollection GetLabelsAt(int index)
        {
            return _contact.GetLabelCollection(_GetIndexedPropety(index));
        }

        public int IndexOfLabels(params string[] labels)
        {
            string node;
            HRESULT hr = ContactUtil.GetLabeledNode((IContactProperties)_contact.IContact, _collection, labels, out node);
            // If a preferred node wasn't found, return a default type.
            if ((HRESULT)Win32Error.ERROR_PATH_NOT_FOUND == hr)
            {
                return -1;
            }

            return ContactUtil.GetIndexFromNode(node);
        }

        private string[] _MakeLabelArray(string label1, string label2, string label3)
        {
            int count = (null == label1 ? 0 : 1) + (null == label2 ? 0 : 1) + (null == label3 ? 0 : 1);
            string[] labels = null;
            if (0 != count)
            {
                labels = new string[count];
                if (null != label3)
                {
                    labels[--count] = label3;
                }
                if (null != label2)
                {
                    labels[--count] = label2;
                }
                if (null != label1)
                {
                    labels[--count] = label1;
                }
            }
            return labels;
        }

        public T this[string label]
        {
            get
            {
                return this[_MakeLabelArray(label, null, null)];
            }
            set
            {
                this[_MakeLabelArray(label, null, null)] = value;
            }
        }

        public T this[string label1, string label2]
        {
            get
            {
                return this[_MakeLabelArray(label1, label2, null)];
            }
            set
            {
                this[_MakeLabelArray(label1, label2, null)] = value;
            }
        }

        public T this[string label1, string label2, string label3]
        {
            get
            {
                return this[_MakeLabelArray(label1, label2, label3)];
            }
            set
            {
                this[_MakeLabelArray(label1, label2, label3)] = value;
            }
        }
        
        public T this[params string[] labels]
        {
            get
            {
                string node;
                HRESULT hr = ContactUtil.GetLabeledNode((IContactProperties)_contact.IContact, _collection, labels, out node);
                // If a preferred node wasn't found, return a default type.
                if ((HRESULT)Win32Error.ERROR_PATH_NOT_FOUND == hr)
                {
                    return default(T);
                }
                // For other errors, throw an exception.
                hr.ThrowIfFailed("An error occurred trying to get the preferred value");

                return _creatorDelegate(_contact, node);
            }
            set
            {
                string node;

                HRESULT hr = ContactUtil.GetLabeledNode((IContactProperties)_contact.IContact, _collection, labels, out node);
                // If a node with the specified labels wasn't found, then we'll just create one.
                if ((HRESULT)Win32Error.ERROR_PATH_NOT_FOUND == hr)
                {
                    ContactUtil.CreateArrayNode((IContactProperties)_contact.IContact, _collection, true, out node)
                        .ThrowIfFailed("Unable to create an arrayNode to set.");

                    if (null != labels)
                    {
                        try
                        {
                            ContactUtil.SetLabels((IContactProperties)_contact.IContact, node, labels)
                                .ThrowIfFailed("Unable to set the labels on the newly created arrayNode.");
                        }
                        catch
                        {
                            // Don't commit partial changes to the contact.
                            _contact.Invalidate();
                            throw;
                        }
                    }

                    // Reset the HRESULT.
                    hr = HRESULT.S_OK;
                }
                // If this failed for a reason other than that the node isn't present, throw the exception.
                hr.ThrowIfFailed("Unable to find the labeled arrayNode to set.");

                // Now we have the node to work with.  Try to set the values.
                _committerDelegate(_contact, node, value);
            }
        }

        public void Add(T item, params string[] labels)
        {
            try
            {
                string node = _contact.AddNode(_collection, true);
                if (null != labels)
                {
                    GetLabelsAt(ContactUtil.GetIndexFromNode(node)).AddRange(labels);
                }
                _committerDelegate(_contact, node, item);
            }
            catch
            {
                _contact.Invalidate();
                throw;
            }
        }

        #endregion

        #region IList<T> Members

        public int IndexOf(T item)
        {
            Predicate<T> doesItemMatch = delegate(T t) { return null == t; };
            if (null != item)
            {
                doesItemMatch = delegate(T t) { return item.Equals(t); };
            }

            int i = 0;
            foreach (T t in this)
            {
                if (doesItemMatch(t))
                {
                    return i;
                }
                ++i;
            }

            return -1;
        }

        void IList<T>.Insert(int index, T item)
        {
            throw new NotSupportedException("This interface doesn't support insertion of elements at arbitrary indexes");
        }

        public void RemoveAt(int index)
        {
            string property = _GetIndexedPropety(index);
            Assert.IsTrue(ContactUtil.DoesPropertyExist((IContactProperties)_contact.IContact, property));
            ContactUtil.DeleteArrayNode((IContactProperties)_contact.IContact, property);
        }

        public T this[int index]
        {
            get
            {
                string property = _GetIndexedPropety(index);
                Assert.IsTrue(ContactUtil.DoesPropertyExist((IContactProperties)_contact.IContact, property));
                return _creatorDelegate(_contact, property);
            }
            set
            {
                string property = _GetIndexedPropety(index);
                Assert.IsTrue(ContactUtil.DoesPropertyExist((IContactProperties)_contact.IContact, property));
                _committerDelegate(_contact, property, value);
            }
        }
        #endregion

        #region ICollection<T> Members

        public void Add(T item)
        {
            Add(item, null);
        }

        public void Clear()
        {
            bool propertyExists = true;
            int i = 1;
            for (; propertyExists; ++i)
            {
                string property = string.Format(_formatNode, i);
                propertyExists = ContactUtil.DoesPropertyExist((IContactProperties)_contact.IContact, property);
                if (propertyExists)
                {
                    _contact.RemoveNode(property);
                }
            }
            // Can cache this since we've already walked it.
            Count = i - 1;
        }

        public bool Contains(T item)
        {
            return -1 != IndexOf(item);
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            Validate.NotNull(array, "array");

            if (arrayIndex + Count >= array.Length)
            {
                throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, "The index implies that the copy should write past the length of the buffer.  The array hasn't been modified as part of this call.");
            }
            
            foreach (T item in this)
            {
                array[arrayIndex] = item;
                ++arrayIndex;
            }
        }

        public int Count
        {
            get
            {
                // can use a cached value here.  If we haven't calculated this before then need to walk properties.
                if (-1 == _count)
                {
                    int i = 0;
                    while (ContactUtil.DoesPropertyExist((IContactProperties)_contact.IContact, string.Format(null, _formatNode, i + 1)))
                    {
                        ++i;
                    }
                    _count = i;
                }
                return _count;
            }
            private set
            {
                _count = value;
            }
        }

        bool ICollection<T>.IsReadOnly
        {
            get { return false; }
        }

        public bool Remove(T item)
        {
            int i = IndexOf(item);
            if (-1 == i)
            {
                return false;
            }
            RemoveAt(i);
            return true;
        }

        #endregion

        #region IEnumerable<T> Members

        public IEnumerator<T> GetEnumerator()
        {
            bool propertyExists = true;
            int i = 1;
            for (; propertyExists; ++i)
            {
                string property = string.Format(_formatNode, i);
                propertyExists = ContactUtil.DoesPropertyExist((IContactProperties)_contact.IContact, property);
                if (propertyExists)
                {
                    yield return _creatorDelegate(_contact, property);
                }
            }
            // Can cache this since we've already walked it.
            Count = i - 1;
        }

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion
    }
}
